package ferp.core.player;

import ferp.core.Version;
import ferp.core.calc.Calculator;
import ferp.core.calc.scorer.Scorer;
import ferp.core.game.Game;
import ferp.core.game.Settings;
import ferp.core.game.Statistics;
import ferp.core.log.Log;
import ferp.core.state.State;
import ferp.core.util.Convert;

import java.io.*;
import java.util.HashMap;
import java.util.Map;

/**
 * User: olegs
 * Date: 25/03/12 23:27
 */
public class Profile
{
  public static final class Games
  {
    public Game initial;
    public Game actual;
    public Game replay;

    private void clear() {initial = actual = replay = null;}
  }

  public static Profile load(File file) throws Exception
  {
    BufferedReader isr = new BufferedReader(new FileReader(file));

    try
    {
      StringBuilder sb = new StringBuilder();
      String line;
      boolean hacked = false;

      // read signature
      String signature = isr.readLine();

      // zero-length string could be a valid signature in case MD5 algorithm can't be resolved
      if (signature == null)
        return null;

      // file starts with json body (without signature line)
      if (signature.startsWith("{"))
      {
        sb.append(signature);
        hacked = true;
        // reset signature to avoid keeping in memory json body
        signature = null;
      }

      // read profile
      while ((line = isr.readLine()) != null)
        sb.append(line);

      String json = sb.toString();
      Profile p = Game.gson.fromJson(json, Profile.class);

      // set profile id (for backward compatibility), hacked flag and signature
      if (p.id == null)
        p.id = file.getName();

      p.hacked = hacked || !Signature.verify(signature, json);
      p.signature = signature;

      Log.debug(Log.TAG, "profile: " + p.nickname + ", id: " + p.id + ", signature: " + signature + ", hacked: " + p.hacked);

      return p;
    }
    finally
    {
      isr.close();
    }
  }

  // id - unique file name
  public String id;
  // hacked - indicates if profile been hacked (changed externally)
  public transient boolean hacked = false;
  // signature
  public transient String signature;
  // use center
  public boolean useCenter = true;
  // center assigned unique profile ID
  public long centerId;
  // user's nickname
  public String nickname;
  // user's password
  public String password = "";
  // game's settings
  public Settings settings = new Settings();
  // indicates if profile was reported as hacked
  public boolean reported;
  // version - changes every deal
  public long version;

  // get file object for the profile
  public File getFileObject() {return new File(Settings.Folders.profiles, id);}

  public String getConfiguration()
  {
    StringBuilder sb = new StringBuilder(6);

    sb.append(settings.gameFlavor.ordinal()).append(settings.passingProgression.ordinal()).append(settings.passingExit.ordinal());
    sb.append(settings.levels[0].ordinal()).append(settings.levels[1].ordinal()).append(settings.tricking10.ordinal());

    return sb.toString();
  }

  // Important!
  // due to the change in configuration code from 3 to 5 digits there may exist
  // an old 3 digit configuration of form nnn00
  // in this case we must merge both configurations
  public Statistics getStatistics()
  {
    String c = getConfiguration();
    Statistics s = statistics.get(c);

    if (s == null)
    {
      s = new Statistics();

      statistics.put(c, s);
    }

    return s;
  }

  public void setStatistics(Statistics s)
  {
    if (s != null)
      statistics.put(getConfiguration(), s);
  }

  public void save() throws IOException
  {
    // generate profile id if needed (used as file name)
    if (id == null)
      id = java.util.UUID.randomUUID().toString();

    FileOutputStream os = new FileOutputStream(getFileObject());
    String json = Game.gson.toJson(this);

    try
    {
      if (!hacked)
        signature = Signature.generate(json);

      if (signature != null)
      {
        os.write(signature.getBytes());
        os.write("\n".getBytes());
      }

      os.write(json.getBytes());
      os.getFD().sync();
    }
    finally
    {
      os.close();
    }
  }

/*
  public void hack() throws IOException
  {
    FileOutputStream os = new FileOutputStream(getFileObject());
    String json = Game.gson.toJson(this);
    hacked = true;
    //signature = "aaaa";
    signature = null;

    try
    {
      //os.write(signature.getBytes());
      //os.write("\n".getBytes());
      os.write(json.getBytes());
      os.getFD().sync();
    }
    finally
    {
      os.close();
    }
  }
*/

  public void delete()
  {
    File f = getFileObject();

    if (f.exists())
    {
      boolean deleted = f.delete();

      Log.debug(Log.TAG, "deleting file " + f.getPath() + ", result: " + deleted);
    }
  }

  public boolean isLandscape() {return settings.isLandscape();}

  public Games games() {return games;}

  // create new game
  public void createGame()
  {
    games.initial = new Game(settings);
    games.actual = games.initial.clone();
    games.replay = null;

    resumeGame();
  }

  // resume existing game
  public void resumeGame()
  {
    Game g = game();

    // resume only if not replaying and not already resuming
    if (g.state() != State.replay && g.state() != State.resume)
      g.set(State.resume);
  }

  public void setInitialGame()
  {
    games.initial = games.actual.clone();
  }

  // return game object depends on replay mode
  public Game game() {return games.replay == null ? games.actual : games.replay;}

  // check if there is a game
  public boolean hasGame() {return games.actual != null;}

  // clear game upon set over
  public void clearGame()
  {
    games.clear();
  }

  // import game from report - called only in debug mode
  public void importGame(Games g, Version v)
  {
    games = g;

    if (v == null || v.code < 35)
      Scorer.balance(games.actual, settings, Calculator.get(settings.gameFlavor));

    if (v == null || v.code < 50)
      update50();
  }

  public Game startReplay()
  {
    //games.replay = games.initial.clone();
    games.replay = games.initial.clone();
    games.replay.replay = true;

    // replayed game must start from the beginning
    games.replay.set(State.replay);

    return games.replay;
  }

  public Game stopReplay()
  {
    //games.initial = null;
    games.replay = null;
    // show actual game score
    games.actual.end();

    return games.actual;
  }

  // update the profile to version 35
  public boolean update35()
  {
    games.actual = game;

    if (games.actual != null)
      Scorer.balance(games.actual, settings, Calculator.get(settings.gameFlavor));

    // clear game object to reduce JSON
    game = null;

    // success
    return true;
  }

  // update the profile to version 45
  public boolean update46()
  {
    // grow the array to the right size
    boolean[] pi = new boolean[Settings.PASSING_INTERRUPTION_OPTIONS_COUNT];

    System.arraycopy(settings.passingInterruptions, 0, pi, 0, settings.passingInterruptions.length);
    settings.passingInterruptions = pi;

    // update configuration to six digit string
    HashMap<String, Statistics> updated = new HashMap<String, Statistics>();

    for (Map.Entry<String, Statistics> e : statistics.entrySet())
      // save updated statistics
      updated.put(Convert.configuration(e.getKey()), e.getValue());

    // replace old statistics
    statistics = updated;

    // force 'WHISTED' tricking10 game to keep consistency
    settings.tricking10 = Settings.Tricking10.WHISTED;

    // success
    return true;
  }

  public boolean update50()
  {
    update50(games.initial);
    update50(games.actual);
    update50(games.replay);

    for (Statistics s : statistics.values())
      s.update50();

    return true;
  }

  public boolean update54()
  {
    // set correct default values for backward compatibility (moved from 'load' method)
    if (settings.gameFlavor == Settings.GameFlavor.ROSTOV)
      settings.whistOn6Spades = Settings.WhistOn6Spades.ARBITRARY;

    hacked = false;

    return true;
  }

  // actual and replay game snapshots
  private Games games = new Games();
  // game object - to support older versions
  private Game game;
  // summaries map
  private Map<String, Statistics> statistics = new HashMap<String, Statistics>();

  private void update50(Game g)
  {
    if (g != null)
    {
      if (g.state.contains("Begin"))
        g.set(State.replay);

      g.statistics.update50();
    }
  }
}
